package com.igoal.utils;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.PropertyAccessException;

import com.igoal.exception.BeanAttributeNotFoundException;

/**
 * Helper class to manipulate on object.
 * 
 * @author Khoi Pham
 * 
 */
public class ObjectHelper {
	private static final Log s_logger = LogFactory.getLog(ObjectHelper.class);
	
	/**
	 * Instantiate an object based on its class name.
	 * 
	 * @param type
	 *            name in string
	 * @return instance of type
	 * @throws InstantiationException
	 * @throws NoSuchFieldException
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 */
	public static Object instantiateObject(String type)
			throws InstantiationException, NoSuchFieldException,
			ClassNotFoundException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException {
		return instantiateObject(Class.forName(type));
	}

	/**
	 * Instantiate an object based on its class type.
	 * 
	 * @param class type
	 * @return instance of type class
	 * @throws InstantiationException
	 * @throws NoSuchFieldException
	 * @throws ClassNotFoundException
	 * @throws IllegalAccessException
	 */
	public static Object instantiateObject(Class<?> type)
			throws InstantiationException, NoSuchFieldException,
			ClassNotFoundException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException {
		return type.getConstructor().newInstance();
	}

	/**
	 * Find all methods declared with an annotation both in this class and its
	 * superclass. Return all methods (without any annotations) if annotation
	 * param = null.
	 * 
	 * @param type
	 *            class
	 * @param annotation
	 *            annotation
	 * @return list of all methods
	 */
	public static List<Method> findAllAnnotationDeclaredMethods(
			Class<? extends Object> cls,
			Class<? extends Annotation> annotationCls) {
		Class<? extends Object> currentClass = cls;
		List<Method> result = new ArrayList<Method>();

		while (!Object.class.equals(currentClass)) {
			for (Method m : currentClass.getMethods()) {
				if (annotationCls == null
						|| (annotationCls != null && m
								.isAnnotationPresent(annotationCls))) {
					result.add(m);
				}
			}
			currentClass = currentClass.getSuperclass();
		}
		return result;
	}

	/**
	 * Copy attributes (through get-set) from src bean to dest bean.
	 * 
	 * @param src
	 *            src bean
	 * @param dest
	 *            dest bean
	 * @param isContinueCopyIfError
	 * 			  stop when attribute not exists or not
	 *             
	 * @throws BeanAttributeNotFoundException
	 *             when attribute exists in src but not in dest	 *             
	 */
	public static void copyBeanAttributes(Object src, Object dest, boolean isContinueCopyIfError)
			throws BeanAttributeNotFoundException {
		BeanWrapper srcBW = new BeanWrapperImpl(src);
		BeanWrapper destBW = new BeanWrapperImpl(dest);

		PropertyDescriptor[] entityProperties = srcBW.getPropertyDescriptors();

		for (PropertyDescriptor srcProperty : entityProperties) {

			try {

				String srcPropertyName = srcProperty.getName();
				Object srcPropertyValue = srcBW.getPropertyValue(srcPropertyName);
				
				destBW.setPropertyValue(srcPropertyName, srcPropertyValue);

			} catch (NotWritablePropertyException nwe) {
				String msg = "There's no set-method for property ["
					+ srcProperty.getName() + "] in "
					+ src.getClass().getName();
				if (!isContinueCopyIfError) {
					s_logger.error(msg);
					throw new BeanAttributeNotFoundException(msg, nwe);
				} else {
					s_logger.debug(msg);
				}
			} catch (RuntimeException re) {
				String msg = "Runtime exception in copy.";
				if (!isContinueCopyIfError) {
					s_logger.error(msg, re);
					throw re;
				} else {
					s_logger.debug(msg + re.getMessage());
				}
			}
		}
	}
	
	/**
	 * Copy properties to another properties have same name but have prefix.
	 * <p>ex: copy from m_username to m_preUsername we use:
	 * <p>copyBeanAttributes(obj, "pre");
	 * @param obj object that works on
	 * @param preFix prefix of destination properties
	 */
	public static void copyBeanAttributes(Object obj, String preFix) {
		if(obj == null || StringUtils.isBlank(preFix)) return;
		
		BeanWrapper bwp = new BeanWrapperImpl(obj);
		PropertyDescriptor[] objProperties = bwp.getPropertyDescriptors();
		
		for (PropertyDescriptor propertyDescriptor : objProperties) {
			String propertyName = propertyDescriptor.getName();
			Object value = bwp.getPropertyValue(propertyName);
			char fchar = propertyName.charAt(0);
			propertyName = String.valueOf(fchar).toUpperCase() + propertyName.substring(1);
			propertyName = preFix + propertyName;
			try{
				PropertyDescriptor pd = bwp.getPropertyDescriptor(propertyName);				
				bwp.setPropertyValue(pd.getName(), value);
			}catch (InvalidPropertyException e) {
				String msg = "Runtime exception in copy";
				s_logger.debug(msg + e.getMessage());
			}catch (PropertyAccessException e){
				String msg = "Runtime exception in copy";
				s_logger.debug(msg + e.getMessage());
			}
		}
	}
	
	public static Map<String, Map<String, Object>> checkBeanAttributesNotSameValue(Object obj, String preFix){
		if(obj == null || StringUtils.isBlank(preFix)) return null;
		
		BeanWrapper bwp = new BeanWrapperImpl(obj);
		PropertyDescriptor[] objProperties = bwp.getPropertyDescriptors();
		
		// map: propertyName <List {Map(old, value), Map(new, value)}>
		Map<String, Map<String, Object>> result = new HashMap<String, Map<String,Object>>();
				
		for (PropertyDescriptor propertyDescriptor : objProperties) {
			String propertyName = propertyDescriptor.getName();
			Object value = bwp.getPropertyValue(propertyName);
			char fchar = propertyName.charAt(0);
			String prePropertyName = preFix + String.valueOf(fchar).toUpperCase() + propertyName.substring(1); 
			try{
				PropertyDescriptor pd = bwp.getPropertyDescriptor(prePropertyName);				
				Object pValue = bwp.getPropertyValue(pd.getName());
				if((value == null && pValue != null) || (value != null && pValue == null) || (value != null && !value.equals(pValue))){
					if(value == null && pValue instanceof String && StringUtils.isBlank((String) pValue)){
						continue;
					}else if(pValue == null && value instanceof String && StringUtils.isBlank((String) value)){
						continue;
					}
					
					if(value instanceof Collection<?> && pValue instanceof Collection<?>){
						boolean flag = true;
						Collection<?> values = (Collection<?>) value;
						Collection<?> pValues = (Collection<?>) pValue;
						
						if(values.size() != pValues.size()){
							flag = false;							
						}
						
						for (Object val : pValues) {
							if(!flag) break;
							if(!pValues.contains(val)){
								flag = false;
							}
						}
						
						if(flag){
							continue;
						}
					}else if(value instanceof Collection<?> || pValue instanceof Collection<?>){
						if(value != null && pValue == null){
							if(((Collection<?>) value).isEmpty()){
								continue;
							}
						}else if(value == null && pValue != null){
							if(((Collection<?>) pValue).isEmpty()){
								continue;
							}
						}
					}
					
					Map<String, Object> vals = new HashMap<String, Object>();
					vals.put("old", pValue);
					vals.put("new", value);
					
					result.put(propertyName, vals);
				}
				
			}catch (InvalidPropertyException e) {
				String msg = "Runtime exception in copy";
				s_logger.debug(msg + e.getMessage());
			}catch (PropertyAccessException e){
				String msg = "Runtime exception in copy";
				s_logger.debug(msg + e.getMessage());
			}
		}
		
		return result;
	}
	
	/**
	 * Dynamically invoke static function by its name
	 * 
	 * @param sFullMethodName full static method name
	 * @param paramTypes param types
	 * @param params
	 * @return result from invoking
	 */
	public static Object invokeStaticMethod(String sFullMethodName, 
			Class<?>[] paramTypes, Object[] params) 
			throws InstantiationException, NoSuchFieldException,
				ClassNotFoundException, IllegalAccessException,
				InvocationTargetException, NoSuchMethodException{
		if (sFullMethodName == null || "".equals(sFullMethodName)) 
			return null;
		int delimiterPoint = sFullMethodName.lastIndexOf('.');
		if (delimiterPoint <= 0) return null;
		
		String clsName = sFullMethodName.substring(0, delimiterPoint);
		String methodName = sFullMethodName.substring(delimiterPoint + 1);
		Class<?> cls = Class.forName(clsName);
		Method m = cls.getDeclaredMethod(methodName, paramTypes);
		return m.invoke(null, params);
	}
}
